/* 动态树 LCT
* 1.动态树问题
    维护一个森林，支持删除某条边，加入某条边，并保证加边，删边后仍是森林。
    我们要维护该森林的一些信息。
    一般的操作有两点连通性 两点路径权值和 连接两点 切断某条边 修改信息 等

* 2.实链剖分
    对于一个点连向它所有儿子的边，我们自己选择一条边进行剖分，我们称被选择的边为 实边，其他边则为 虚边。
    对于实边，我们称它所连接的儿子为实儿子。
    对于一条由实边组成的链，我们同样称之为实链。
    对于每条实链，分别建一个 Splay 来维护整个链区间的信息。
    正是因为实链我们可以自己任意选择，使得实链剖分可以适用于动态树的问题

* 3.辅助树splay
    splay维护所有实边路径，中序遍历就是维护的路径(实链上节点的深度递增顺序)
    本质上维护所有实边，用splay中的后继和前驱来维护原树中的父子关系
    原树中的虚链都在辅助树中，子节点所在splay的father指向父节点，但是父节点的两个儿子不指向子节点
    父节点: 连接不同的实链
        在同一辅助树内，父节点指向splay中的上层节点
        跨辅助树(虚边)，指向不同splay树中的节点(另一条实链上的点)
    splay的左孩子指向实链中的前一个节点，有孩子指向实链中的下一个节点
    虚边:splay的根节点来维护

* 4.splay op
    (1)access(x) 建立一条从根节点到x的实边路径
    (2)makeroot(x) 将x变为根节点
    (3)findroot(x) 找到x所在树的根节点
    (4)split(x, y) 将x到y的路径变为实边路径
    (5)link(x, y) 若x,y不通，则加入(x,y)这条边
    (6)cut(x, y) 若(x,y)这条边存在，删除

* 本题:
    出现环->断环成链，断开权值最大的边
*/

#define DEBUG
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
const int N=15e4+10, INF=0x3f3f3f3f;

#define l(u) tree[u].s[0]
#define r(u) tree[u].s[1]
#define L(u) tree[tree[u].s[0]]
#define R(u) tree[tree[u].s[1]]

inline int read()
{
	int x=0;char c=getchar();
	while (c<'0'||c>'9') c=getchar();
	while (c>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48,c=getchar();
	return x;
}
inline void write(int x)
{
    if(x<0) printf("-"), x*=-1;
	if (x>=10) write(x/10);
	putchar(x%10+48);
}//快读快输

struct Edge
{
    int u, v, a, b;
    bool operator<(const Edge &e) const
    {
        return a<e.a;
    }
}e[N];

struct Node
{
    int s[2], pa, val; //b
    int mx; //以当前节点为根的子树中val最大的节点编号
    int rev; //反转懒标记
}tree[N<<2];
int n, m;
int stk[N], fa[N]; //并查集父节点数组

inline void printSeg()
{
    for(int i=1; tree[i].s[0]!=0; i++)
                printf("test:%d [%d, %d, %d, %d, %d, %lld]\n", i, tree[i].s[0], tree[i].s[1], tree[i].pa, tree[i].val, tree[i].rev, tree[i].mx);
}

bool isRoot(int u) //u是否为实链的顶部
{
    return tree[tree[u].pa].s[0] != u && tree[tree[u].pa].s[1] != u;
}

int find(int u)
{
    return u==fa[u]?u:fa[u]=find(fa[u]);
}

// void merge(int x, int y)
// {
//     fa[find(x)]=find(y);
// }

//-------------Splay----------------------

inline void pushup(int u) //更新当前节点的mx
{
    tree[u].mx=u;
    for(int i=0; i<2; i++)
        //当前节点孩子中最大权值>自身存在的最大权值
        if(tree[tree[tree[u].s[i]].mx].val > tree[tree[u].mx].val)
        tree[u].mx=tree[tree[u].s[i]].mx;
}

inline void pushrev(int u)
{
    swap(l(u), r(u)); //执行反转操作
    tree[u].rev^=1;
}

void pushdown(int u)
{
    auto &U=tree[u], &L=L(u), &R=R(u);
    if(U.rev)
    {
        pushrev(l(u));
        pushrev(r(u));
        U.rev=0;
    }
}

void rotate(int x) //x上移一层
{
    int y=tree[x].pa, z=tree[y].pa;
    int k=tree[y].s[1]==x;
    //x,y互换
    if(!isRoot(y)) tree[z].s[tree[z].s[1]==y]=x;
    tree[x].pa=z;
    //将x的孩子节点接到y上
    tree[y].s[k]=tree[x].s[k^1], tree[tree[x].s[k^1]].pa=y;
    tree[x].s[k^1]=y, tree[y].pa=x;
    pushup(y), pushup(x);
}

void splay(int x) //维护平衡
{
    int top=0, u=x;
    stk[++top]=u;
    while(!isRoot(u)) stk[++top]=u=tree[u].pa;
    while(top) pushdown(stk[top--]);

    while(!isRoot(x))
    {
        int y=tree[x].pa, z=tree[y].pa;
        if(!isRoot(y))
            if((tree[z].s[1]==y)^(tree[y].s[1]==x)) rotate(x); //不在一条直线上
            else rotate(y);
        //此时x，x的父节点，x的祖父节点一定在一条直线上
        rotate(x);
    }
}

//-------------LCT----------------------
void access(int x) //建立一条从根节点到x的实链，且将x变为splay的根节点
{
    int z=x; //初始节点编号
    for(int y=0; x; y=x, x=tree[x].pa) //x沿着虚边向上找根节点
    {
        splay(x); //找到当前辅助树的根
        tree[x].s[1]=y, pushup(x); //按照实链顺序布置splay
    }
    splay(z); //将初始节点转到根

}

void makeRoot(int x) //将x变为原树根节点 且左子树为空
{
    access(x); //x为辅助树根节点，反转中序遍历
    pushrev(x);
}

int findRoot(int x)//找到x所在的原树的根节点，再将原树的根节点旋转到辅助树的根节点
{
    access(x); //建立根节点到x的实链，x位于辅助树的根节点位置
    while(tree[x].s[0]) pushdown(x), x=tree[x].s[0]; //找到实链根节点
    splay(x); //转到根节点
    return x;
}

void split(int x, int y) //将x->y转为实链
{
    makeRoot(x);
    access(y);
}

void link(int x, int y) //若x,y不通，则加入(x,y)这条边
{
    makeRoot(x);
    if(findRoot(y)!=x) tree[x].pa=y; //如果x和y不在同一条实链上，则建立连接 
}

void cut(int x, int y)
{
    makeRoot(x);
    //确保(x,y)是实边
    if(findRoot(y)==x && tree[x].s[1]==y && !tree[y].s[0]) //x->y && y没有上级((x,y)不是实边) 
    {
        tree[y].pa=tree[x].s[1]=0;
        pushup(x);
    }
}

void solve()
{
    n=read(); m=read();
    for(int i=1;i<=n+m;i++) fa[i]=i;
    for(int i=1; i<=m; i++)
    {
        e[i].u=read(); e[i].v=read();
        e[i].a=read(); e[i].b=read();
    } 
    sort(e+1, e+1+m);

    for(int i=1; i<=n+m; i++)
    {
        if(i>n) tree[i].val=e[i-n].b; //图中的边
        tree[i].mx=i; //最开始每棵树的splay最大是自己
    }

    int ans=INF;
    for(int i=1; i<=m; i++) //加入每条边
    {
        int u=e[i].u, v=e[i].v, a=e[i].a, b=e[i].b;
        if(find(u)==find(v)) //当前已经连接，断环成链
        {
            split(u, v);
            int t=tree[v].mx;
            if(tree[t].val>b)
            {
                cut(e[t-n].u, t), cut(t, e[t-n].v); //当前最大权值需要更新
                link(u, n+i), link(n+i, v); //在树中更新最大值
            }
        }
        else
        {
            fa[find(u)]=find(v); //先连接，在成链
            link(u, n+i); link(n+i, v);
        }
        if(find(1)==find(n))
        {
            split(1, n);
            ans=min(ans, a+tree[tree[n].mx].val);
            // printf("%d %d->%d %d %d %d+%d\n", ans, u, v, a, b, a, tree[tree[n].mx].val);
        }
    }
    write(ans==INF?-1:ans); puts("");
}

signed main()
{
    #ifdef DEBUG
        freopen("./in.txt","r",stdin);
        freopen("./out.txt","w",stdout);
    #endif
    int T=1;
    // T=read();
    while(T--)
        solve();

    return 0;
}